package com.gitee.wsl.eventbus

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

/** A function to dispatch a Msg to the runtime in the runtime context. */
typealias Dispatch<Msg> = (Msg) -> Unit

/**
 * A function to run a side-effect in the effect context. Provides a Dispatch function to dispatch a
 * Msg to the runtime in the runtime context.
 */
typealias Effect<Msg> = suspend CoroutineScope.(Dispatch<Msg>) -> Any?

/** [Effect] builder function. */
fun <Msg> effect(
    block: Effect<Msg>,
): Effect<Msg> = block

/** Init builder function. */
fun <Model, Msg> init(
    block: () -> Pair<Model, Effect<Msg>>,
): () -> Pair<Model, Effect<Msg>> = block

/** Update builder function. */
fun <Model, Msg> update(
    block: (Msg, Model) -> Pair<Model, Effect<Msg>>,
): (Msg, Model) -> Pair<Model, Effect<Msg>> = block

/** View builder function. */
fun <Model, Msg, Props> view(
    block: (Model, Dispatch<Msg>) -> Props,
): (Model, Dispatch<Msg>) -> Props = block

/** Render builder function. */
fun <Props> render(
    block: (Props) -> Any?,
): (Props) -> Any? = block

/** Create a pair of [model] to [effect] where [effect] defaults to [none]. */
fun <Model, Msg> next(model: Model, effect: Effect<Msg> = none()): Pair<Model, Effect<Msg>> =
    model to effect

/** Create an empty [Effect]. */
fun <Msg> none(): Effect<Msg> = {}

/** Compose [effects] into a single [Effect]. */
fun <Msg> batch(vararg effects: Effect<Msg>): Effect<Msg> = batch(effects.asIterable())

/** Compose [effects] into a single [Effect]. */
fun <Msg> batch(effects: Iterable<Effect<Msg>>): Effect<Msg> = { dispatch ->
    for (effect in effects) launch { effect(dispatch) }
}

/** Map [effect] of type [A] to [Effect] of [B] using [f]. */
fun <A, B> map(effect: Effect<A>, f: (A) -> B): Effect<B> = { dispatch ->
    effect { a -> dispatch(f(a)) }
}

/**
 * Contramap from [Dispatch] of [A] to [Dispatch] of [B]
 *
 * See: [https://oolong-kt.org/recipes/navigation/](https://oolong-kt.org/recipes/navigation/)
 */
fun <A, B> contramap(dispatch: Dispatch<A>, f: (B) -> A): Dispatch<B> = { b -> dispatch(f(b)) }


interface RuntimeScope<Model, Msg, Props>{
    val runtimeContext: CoroutineContext
    val renderContext: CoroutineContext
    val effectContext: CoroutineContext
    fun init(block:() -> Pair<Model, Effect<Msg>>)
    fun update(block: (Msg, Model) -> Pair<Model, Effect<Msg>>)
    fun view(block: (Model, Dispatch<Msg>) -> Props)
    fun render(block:(Props) -> Any?)
}

class RuntimeScopeImp<Model, Msg, Props>():RuntimeScope<Model, Msg, Props>{
    override val runtimeContext: CoroutineContext = Dispatchers.Default
    override val renderContext: CoroutineContext = Dispatchers.Main
    override val effectContext: CoroutineContext = Dispatchers.Default

    var initBlock:(() -> Pair<Model, Effect<Msg>>)?=null
    var updateBlock:((Msg, Model) -> Pair<Model, Effect<Msg>>)?=null
    var viewBlock:((Model, Dispatch<Msg>) -> Props)?=null
    var renderBlock:((Props) -> Any?)?=null

    override fun render(block: (Props) -> Any?) {
        renderBlock = block
    }

    override fun view(block: (Model, Dispatch<Msg>) -> Props) {
       viewBlock = block
    }

    override fun update(block: (Msg, Model) -> Pair<Model, Effect<Msg>>) {
        updateBlock = block
    }

    override fun init(block: () -> Pair<Model, Effect<Msg>>) {
        initBlock = block
    }

    fun check(){
        if(initBlock==null||updateBlock==null||viewBlock==null||renderBlock==null)
            throw IllegalArgumentException("fun must set")
    }
}

fun <Model, Msg, Props> sampleMVI(block:RuntimeScope<Model, Msg, Props>.()->Unit): Job{
   val scope = RuntimeScopeImp<Model, Msg, Props>()
   block(scope)
   scope.check()
   return sampleMVI<Model, Msg, Props>(
       init = scope.initBlock!!,
       update = scope.updateBlock!!,
       view = scope.viewBlock!!,
       render = scope.renderBlock!!,
       runtimeContext= scope.runtimeContext,
       renderContext = scope.renderContext,
       effectContext = scope.effectContext
   )
}


/** Create a runtime. */
fun <Model, Msg, Props> sampleMVI(
    init: () -> Pair<Model, Effect<Msg>>,
    update: (Msg, Model) -> Pair<Model, Effect<Msg>>,
    view: (Model, Dispatch<Msg>) -> Props,
    render: (Props) -> Any?,
    runtimeContext: CoroutineContext = Dispatchers.Default,
    renderContext: CoroutineContext = Dispatchers.Main,
    effectContext: CoroutineContext = Dispatchers.Default
): Job {
    val runtime =
        RuntimeImpl(
            init,
            update,
            view,
            { model, _ -> render(model) },
            runtimeContext,
            renderContext,
            effectContext,
        )
    return runtime.job
}

private class RuntimeImpl<Model, Msg, Props>(
    init: () -> Pair<Model, Effect<Msg>>,
    private val update: (Msg, Model) -> Pair<Model, Effect<Msg>>,
    private val view: (Model, Dispatch<Msg>) -> Props,
    private val render: (Props, Dispatch<Msg>) -> Any?,
    private val runtimeContext: CoroutineContext,
    private val renderContext: CoroutineContext,
    private val effectContext: CoroutineContext,
) : CoroutineScope {

    val job: Job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = runtimeContext + job

    private var currentState: Model

    init {
        val initNext = init()
        currentState = initNext.first
        launch(runtimeContext) { step(initNext) }
    }

    private fun dispatch(msg: Msg) {
        if (isActive) {
            launch(runtimeContext) { step(update(msg, currentState)) }
        }
    }

    private fun step(next: Pair<Model, Effect<Msg>>) {
        val (state, effect) = next
        val props = view(state, ::dispatch)
        currentState = state
        launch(renderContext) { render(props, ::dispatch) }
        launch(effectContext) { effect(::dispatch) }
    }
}
